package net.lindeer.lang

internal class Scanner(private val source: String) {
    private val mTokens = mutableListOf<Token>()
    private var start = 0
    private var current = 0
    private var line = 0

    companion object {
        private fun isDigit(c: Char): Boolean {
            return c in '0'..'9'
        }

        private fun isAlpha(c: Char): Boolean {
            return c in 'a'..'z' || c in 'A'..'Z' || c == '_'
        }

        private val reserved by lazy {
            Token.keywords.map { type -> type.name.toLowerCase() to type }.toMap()
        }
    }

    fun scanTokens(): List<Token> {
        while (!isEnd) {
            start = current
            val token = scanToken()
            if (token != null) {
                mTokens.add(token)
            }
        }

        mTokens.add(Token(Token.Type.EOF, "", null, line))
        return mTokens
    }

    private val isEnd: Boolean
        get() = current >= source.length

    private fun scanToken(): Token? {
        return when (val c = advance) {
            '(' -> makeToken(Token.Type.LEFT_PAREN)
            ')' -> makeToken(Token.Type.RIGHT_PAREN)
            '{' -> makeToken(Token.Type.LEFT_BRACE)
            '}' -> makeToken(Token.Type.RIGHT_BRACE)
            ',' -> makeToken(Token.Type.COMMA)
            '.' -> makeToken(Token.Type.DOT)
            '-' -> makeToken(Token.Type.MINUS)
            '+' -> makeToken(Token.Type.PLUS)
            ';' -> makeToken(Token.Type.SEMICOLON)
            '*' -> makeToken(Token.Type.STAR)
            '!' -> makeToken(if (match('=')) Token.Type.NOT_EQUAL else Token.Type.NOT)
            '=' -> makeToken(if (match('=')) Token.Type.EQUAL_EQUAL else Token.Type.EQUAL)
            '<' -> makeToken(if (match('=')) Token.Type.LESS_EQUAL else Token.Type.LESS)
            '>' -> makeToken(if (match('=')) Token.Type.GREATER_EQUAL else Token.Type.GREATER)
            '/' -> {
                if (match('/')) {
                    while (peek != '\n' && !isEnd) {
                        advance
                    }
                    null
                } else {
                    makeToken(Token.Type.SLASH)
                }
            }
            ' ', '\r', '\t' -> null
            '\n' -> {
                line++
                null
            }
            '"' -> makeStringToken()
            else -> {
                when {
                    isDigit(c) -> {
                        makeNumberToken()
                    }
                    isAlpha(c) -> {
                        makeIdentifierToken()
                    }
                    else -> {
                        Rox.error(line, "Unexpected character!")
                        null
                    }
                }
            }
        }
    }

    private fun match(expected: Char): Boolean {
        return if (isEnd || source[current] != expected) {
            false
        } else {
            current++
            true
        }
    }

    private val peek: Char
        get() = if (isEnd) Char.MIN_VALUE else source[current]

    private val advance: Char
        get() = source[current++]

    private val nextPeek: Char
        get() = if (current + 1 >= source.length) Char.MIN_VALUE else source[current + 1]

    private fun makeToken(type: Token.Type, literal: Any? = null): Token {
        val text = source.substring(start, current)
        return Token(type, text, literal, line)
    }

    private fun makeStringToken(): Token? {
        while (peek != '"' && !isEnd) {
            if (peek == '\n') {
                line++
            }
            advance
        }
        if (isEnd) {
            Rox.error(line, "Unterminated string!")
            return null
        }
        val value = source.substring(start + 1, current)
        advance
        return makeToken(Token.Type.STRING, value)
    }

    private fun makeNumberToken(): Token {
        while (isDigit(peek)) {
            advance
        }
        if (peek == '.' && isDigit(nextPeek)) {
            advance
            while (isDigit(peek)) {
                advance
            }
        }
        return makeToken(Token.Type.NUMBER, source.substring(start, current).toDouble())
    }

    private fun makeIdentifierToken(): Token {
        while (isAlpha(peek) || isDigit(peek)) {
            advance
        }
        val text = source.substring(start, current)
        val type = reserved[text] ?: Token.Type.IDENTIFIER
        return makeToken(type)
    }
}
